package com.google.ytd;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore.Video;

import com.google.ytd.Authorizer.AuthorizationListener;
import com.jason.unpluggedsuperstar.R;

public class YTDUploader extends YTD {

	private static final int MAX_RETRIES = 5;
	private static final int BACKOFF = 4; // base of exponential backoff
	private static final String INITIAL_UPLOAD_URL = "http://uploads.gdata.youtube.com/resumable/feeds/api/users/default/uploads";
	private static final String DEFAULT_VIDEO_CATEGORY = "Entertainment";
	private static final String DEFAULT_VIDEO_TAGS = "mobile";

	/*
	 * private var
	 */
	private Activity mActivity = null;
	private Context mContext = null;
	private Authorizer authorizer = null;
	private YTD mYTD = null;
	private Uri videoUri = null;
	private String ytdDomain = null;
	private String youTubeName = null;
	private String clientLoginToken = null;
	private String mTitle = null;
	private String mDescription = null;
	private progressListener mProgressListener = null;

	// TODO
	private String assignmentId = "";
	private double currentFileSize = 0;
	private double totalBytesUploaded = 0;
	private int numberOfRetries = 0;

	static class YouTubeAccountException extends Exception {
		public YouTubeAccountException(String msg) {
			super(msg);
		}
	}

	public YTDUploader(Activity mActivity) {
		super(mActivity.getApplicationContext());

		this.mActivity = mActivity;
		this.mContext = mActivity.getApplicationContext();
		this.authorizer = new GlsAuthorizer.GlsAuthorizerFactory()
				.getAuthorizer(mContext, GlsAuthorizer.YOUTUBE_AUTH_TOKEN_TYPE);
	}

	/*
	 * class
	 */
	class ResumeInfo {
		int nextByteToUpload;
		String videoId;

		ResumeInfo(int nextByteToUpload) {
			this.nextByteToUpload = nextByteToUpload;
		}

		ResumeInfo(String videoId) {
			this.videoId = videoId;
		}
	}

	/*
	 * private
	 */
	private void getAuthTokenWithPermission(String accountName,
			AuthorizationListener<String> al) {
		this.authorizer.fetchAuthToken(accountName, mActivity, al);
	}

	private void uploadVideo(Uri videoUri) {
		Handler handler = new Handler() {
			@Override
			public void handleMessage(Message msg) {
				String videoId = msg.getData().getString("videoId");

				if (!Util.isNullOrEmpty(videoId)) {
					currentFileSize = 0;
					totalBytesUploaded = 0;
					mProgressListener.onSuccess(videoId);
				} else {
					String error = msg.getData().getString("error");
					if (!Util.isNullOrEmpty(error)) {
						mProgressListener.onError(error);
					}
				}
			}
		};

		asyncUpload(videoUri, handler);
	}

	public void asyncUpload(final Uri uri, final Handler handler) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				Message msg = new Message();
				Bundle bundle = new Bundle();
				msg.setData(bundle);

				String videoId = null;
				int submitCount = 0;
				try {
					while (submitCount <= MAX_RETRIES && videoId == null) {
						try {
							submitCount++;
							videoId = startUpload(uri);
							assert videoId != null;
						} catch (Internal500ResumeException e500) { // TODO -
																	// this
																	// should
																	// not
																	// really
																	// happen
							if (submitCount < MAX_RETRIES) {
							} else {
								throw new IOException(e500.getMessage());
							}
						}
					}
				} catch (IOException e) {
					e.printStackTrace();
					bundle.putString("error", e.getMessage());
					handler.sendMessage(msg);
					return;
				} catch (YouTubeAccountException e) {
					e.printStackTrace();
					bundle.putString("error", e.getMessage());
					handler.sendMessage(msg);
					return;
				} catch (SAXException e) {
					e.printStackTrace();
					bundle.putString("error", e.getMessage());
					handler.sendMessage(msg);
				} catch (ParserConfigurationException e) {
					e.printStackTrace();
					bundle.putString("error", e.getMessage());
					handler.sendMessage(msg);
				}

				bundle.putString("videoId", videoId);
				handler.sendMessage(msg);
			}
		}).start();
	}

	private String startUpload(Uri uri) throws IOException,
			YouTubeAccountException, SAXException,
			ParserConfigurationException, Internal500ResumeException {
		File file = getFileFromUri(uri);

		if (this.clientLoginToken == null) {
			// The stored gmail account is not linked to YouTube
			throw new YouTubeAccountException(this.youTubeName
					+ " is not linked to a YouTube account.");
		}

		String uploadUrl = uploadMetaData(file.getAbsolutePath(), true);

		this.currentFileSize = file.length();
		this.totalBytesUploaded = 0;
		this.numberOfRetries = 0;

		int uploadChunk = 1024 * 1024 * 3; // 3MB

		int start = 0;
		int end = -1;

		String videoId = null;
		double fileSize = this.currentFileSize;
		while (fileSize > 0) {
			if (fileSize - uploadChunk > 0) {
				end = start + uploadChunk - 1;
			} else {
				end = start + (int) fileSize - 1;
			}
			try {
				videoId = gdataUpload(file, uploadUrl, start, end);
				fileSize -= uploadChunk;
				start = end + 1;
				this.numberOfRetries = 0; // clear this counter as we had a
											// succesfull upload
			} catch (IOException e) {
				ResumeInfo resumeInfo = null;
				do {
					if (!shouldResume()) {
						throw e;
					}
					try {
						resumeInfo = resumeFileUpload(uploadUrl);
					} catch (IOException re) {
					}
				} while (resumeInfo == null);
				if (resumeInfo.videoId != null) { // upload actually complted
													// despite the exception
					videoId = resumeInfo.videoId;
					break;
				} else {
					int nextByteToUpload = resumeInfo.nextByteToUpload;
					this.totalBytesUploaded = nextByteToUpload; // possibly
																// rolling back
																// the
																// previosuly
																// saved value
					fileSize = this.currentFileSize - nextByteToUpload;
					start = nextByteToUpload;
				}
			}
		}

		if (videoId != null) {
			return videoId;
		}

		return null;
	}

	private File getFileFromUri(Uri uri) throws IOException {
		Cursor cursor = mActivity.managedQuery(uri, null, null, null, null);
		if (cursor.getCount() == 0) {
			throw new IOException(String.format("cannot find data from %s",
					uri.toString()));
		} else {
			cursor.moveToFirst();
		}

		String filePath = cursor.getString(cursor
				.getColumnIndex(Video.VideoColumns.DATA));

		File file = new File(filePath);
		cursor.close();
		return file;
	}

	private String uploadMetaData(String filePath, boolean retry)
			throws IOException {
		String uploadUrl = INITIAL_UPLOAD_URL;

		HttpURLConnection urlConnection = getGDataUrlConnection(uploadUrl);
		urlConnection.setRequestMethod("POST");
		urlConnection.setDoOutput(true);
		urlConnection
				.setRequestProperty("Content-Type", "application/atom+xml");
		urlConnection.setRequestProperty("Slug", filePath);
		String atomData;

		String category = DEFAULT_VIDEO_CATEGORY;
		String tags = DEFAULT_VIDEO_TAGS;

		String template = Util.readFile(this.mActivity, R.raw.gdata).toString();
		atomData = String.format(template, mTitle, mDescription, category, "");

		OutputStreamWriter outStreamWriter = new OutputStreamWriter(
				urlConnection.getOutputStream());
		outStreamWriter.write(atomData);
		outStreamWriter.close();

		int responseCode = urlConnection.getResponseCode();
		if (responseCode < 200 || responseCode >= 300) {
			// The response code is 40X
			if ((responseCode + "").startsWith("4") && retry) {
				this.clientLoginToken = authorizer.getFreshAuthToken(
						youTubeName, clientLoginToken);
				// Try again with fresh token
				return uploadMetaData(filePath, false);
			} else {
				throw new IOException(String.format(
						"response code='%s' (code %d)" + " for %s",
						urlConnection.getResponseMessage(), responseCode,
						urlConnection.getURL()));
			}
		}

		return urlConnection.getHeaderField("Location");
	}

	private HttpURLConnection getGDataUrlConnection(String urlString)
			throws IOException {
		URL url = new URL(urlString);
		HttpURLConnection connection = (HttpURLConnection) url.openConnection();
		connection.setRequestProperty("Authorization",
				String.format("GoogleLogin auth=\"%s\"", clientLoginToken));
		connection.setRequestProperty("GData-Version", "2");
		connection.setRequestProperty("X-GData-Client",
				mActivity.getString(R.string.client_id));
		connection.setRequestProperty("X-GData-Key",
				String.format("key=%s", mActivity.getString(R.string.dev_key)));
		return connection;
	}

	private String gdataUpload(File file, String uploadUrl, int start, int end)
			throws IOException {
		int chunk = end - start + 1;
		int bufferSize = 1024;
		byte[] buffer = new byte[bufferSize];
		FileInputStream fileStream = new FileInputStream(file);

		HttpURLConnection urlConnection = getGDataUrlConnection(uploadUrl);
		// some mobile proxies do not support PUT, using X-HTTP-Method-Override
		// to get around this problem
		if (isFirstRequest()) {
			urlConnection.setRequestMethod("POST");
		} else {
			urlConnection.setRequestMethod("POST");
			urlConnection.setRequestProperty("X-HTTP-Method-Override", "PUT");
		}
		urlConnection.setDoOutput(true);
		urlConnection.setFixedLengthStreamingMode(chunk);
		urlConnection.setRequestProperty("Content-Type", "video/3gpp");
		urlConnection.setRequestProperty("Content-Range",
				String.format("bytes %d-%d/%d", start, end, file.length()));

		OutputStream outStreamWriter = urlConnection.getOutputStream();

		fileStream.skip(start);

		int bytesRead;
		int totalRead = 0;
		while ((bytesRead = fileStream.read(buffer, 0, bufferSize)) != -1) {
			outStreamWriter.write(buffer, 0, bytesRead);
			totalRead += bytesRead;
			this.totalBytesUploaded += bytesRead;

			double percent = (totalBytesUploaded / currentFileSize) * 99;
			if (mProgressListener != null)
				mProgressListener.onProgressUpdate(percent);

			if (totalRead == (end - start + 1)) {
				break;
			}
		}

		outStreamWriter.close();

		int responseCode = urlConnection.getResponseCode();

		try {
			if (responseCode == 201) {
				String videoId = parseVideoId(urlConnection.getInputStream());

				String latLng = null;

				submitToYtdDomain(this.ytdDomain, this.assignmentId, videoId,
						this.youTubeName, this.clientLoginToken, this.mTitle,
						this.mDescription, new Date(), latLng, "");
				return videoId;
			} else if (responseCode == 200) {
				Set<String> keySet = urlConnection.getHeaderFields().keySet();
				String keys = urlConnection.getHeaderFields().keySet()
						.toString();
				throw new IOException(
						String.format(
								"Unexpected response code : responseCode=%d responseMessage=%s",
								responseCode,
								urlConnection.getResponseMessage()));
			} else {
				if ((responseCode + "").startsWith("5")) {
					String error = String.format(
							"responseCode=%d responseMessage=%s", responseCode,
							urlConnection.getResponseMessage());
					throw new IOException(error);
				} else if (responseCode == 308) {
				}
			}
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		}

		return null;
	}

	private boolean isFirstRequest() {
		return totalBytesUploaded == 0;
	}

	private String parseVideoId(InputStream atomDataStream)
			throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory
				.newInstance();
		DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
		Document doc = docBuilder.parse(atomDataStream);

		NodeList nodes = doc.getElementsByTagNameNS("*", "*");
		for (int i = 0; i < nodes.getLength(); i++) {
			Node node = nodes.item(i);
			String nodeName = node.getNodeName();
			if (nodeName != null && nodeName.equals("yt:videoid")) {
				return node.getFirstChild().getNodeValue();
			}
		}
		return null;
	}

	public void submitToYtdDomain(String ytdDomain, String assignmentId,
			String videoId, String youTubeName, String clientLoginToken,
			String title, String description, Date dateTaken,
			String videoLocation, String tags) {

		JSONObject payload = new JSONObject();
		try {
			payload.put("method", "NEW_MOBILE_VIDEO_SUBMISSION");
			JSONObject params = new JSONObject();

			params.put("videoId", videoId);
			params.put("youTubeName", youTubeName);
			params.put("clientLoginToken", clientLoginToken);
			params.put("title", title);
			params.put("description", description);
			params.put("videoDate", dateTaken.toString());
			params.put("tags", tags);

			if (videoLocation != null) {
				params.put("videoLocation", videoLocation);
			}

			if (assignmentId != null) {
				params.put("assignmentId", assignmentId);
			}

			payload.put("params", params);
		} catch (JSONException e) {
			e.printStackTrace();
		}

		String jsonRpcUrl = "http://" + ytdDomain + "/jsonrpc";
		String json = Util.makeJsonRpcCall(jsonRpcUrl, payload);

		if (json != null) {
			try {
				JSONObject jsonObj = new JSONObject(json);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}
	}

	private boolean shouldResume() {
		this.numberOfRetries++;
		if (this.numberOfRetries > MAX_RETRIES) {
			return false;
		}
		try {
			int sleepSeconds = (int) Math.pow(BACKOFF, this.numberOfRetries);
			Thread.currentThread().sleep(sleepSeconds * 1000);
		} catch (InterruptedException se) {
			se.printStackTrace();
			return false;
		}
		return true;
	}

	private ResumeInfo resumeFileUpload(String uploadUrl) throws IOException,
			ParserConfigurationException, SAXException,
			Internal500ResumeException {
		HttpURLConnection urlConnection = getGDataUrlConnection(uploadUrl);
		urlConnection.setRequestProperty("Content-Range", "bytes */*");
		urlConnection.setRequestMethod("POST");
		urlConnection.setRequestProperty("X-HTTP-Method-Override", "PUT");
		urlConnection.setFixedLengthStreamingMode(0);

		HttpURLConnection.setFollowRedirects(false);

		urlConnection.connect();
		int responseCode = urlConnection.getResponseCode();

		if (responseCode >= 300 && responseCode < 400) {
			int nextByteToUpload;
			String range = urlConnection.getHeaderField("Range");
			if (range == null) {
				nextByteToUpload = 0;
			} else {
				String[] parts = range.split("-");
				if (parts.length > 1) {
					nextByteToUpload = Integer.parseInt(parts[1]) + 1;
				} else {
					nextByteToUpload = 0;
				}
			}
			return new ResumeInfo(nextByteToUpload);
		} else if (responseCode >= 200 && responseCode < 300) {
			return new ResumeInfo(parseVideoId(urlConnection.getInputStream()));
		} else if (responseCode == 500) {
			// TODO this is a workaround for current problems with resuming
			// uploads while switching transport (Wifi->EDGE)
			throw new Internal500ResumeException(
					String.format("Unexpected response for PUT to %s: %s "
							+ "(code %d)", uploadUrl,
							urlConnection.getResponseMessage(), responseCode));
		} else {
			throw new IOException(
					String.format("Unexpected response for PUT to %s: %s "
							+ "(code %d)", uploadUrl,
							urlConnection.getResponseMessage(), responseCode));
		}
	}

	/*
	 * public
	 */
	public void setProgressListener(progressListener pl) {
		this.mProgressListener = pl;
	}

	public void upload(Uri videoUr, String title, String description) {

		// Check Internet Access
		if (!YTD.haveWifiConnection(mContext)) {
			this.mProgressListener.onError("no wifi connection");
			return;
		}

		this.videoUri = videoUr;
		this.ytdDomain = super.getYTDDomain();
		this.mTitle = title;
		this.mDescription = description;
		this.youTubeName = super.getUserAccount();

		this.getAuthTokenWithPermission(youTubeName,
				new AuthorizationListener<String>() {
					@Override
					public void onCanceled() {
						mProgressListener.onError("auth cancelled");
					}

					@Override
					public void onError(Exception e) {
						mProgressListener.onError(e.getMessage());
					}

					@Override
					public void onSuccess(String result) {
						clientLoginToken = result;
						uploadVideo(videoUri);
					}
				});
	}

	/*
	 * interface
	 */
	public interface progressListener {
		public void onProgressUpdate(double progress);

		public void onError(String error);

		public void onSuccess(String videoId);
	}

	/*
	 * Error : Need this for now to trigger entire upload transaction retry
	 */
	class Internal500ResumeException extends Exception {
		Internal500ResumeException(String message) {
			super(message);
		}
	}

}
